import {resolve} from 'path';
import {BannerPlugin, type Compiler, type Configuration, DefinePlugin, type RuleSetRule} from 'webpack';
import 'webpack-dev-server';
import {VueLoaderPlugin} from 'vue-loader';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import dayjs from 'dayjs';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import TerserPlugin from 'terser-webpack-plugin';
import CopyPlugin from 'copy-webpack-plugin';
import {createReadStream, createWriteStream, existsSync} from 'fs';
import readline from 'readline';

interface EnvType {
  // 打包为库（UMD）
  lib?: boolean,
  // 打包为库（ESM）
  esm?: boolean,
  // 打包为兼容包（使用babel，兼容性工程umd包如果不兼容，需手动将html中的引入添加到打包内容并取消externals）
  old?: boolean
}

interface ExternalConfig {
  // 原始路径
  originalPath: string,
  // 开发阶段原始路径
  originalPathDev?: string,
  // 目标名称（用于改名例如index.js更改为具体的名称以避免重复）
  targetName?: string
}

interface ExternalRuntime {
  // 注入类型
  type: 'js' | 'css',
  // 目标路径
  targetPath: string
}

const version = dayjs().format('YYYY-MM-DD HH:mm:ss');
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const defaultName = process.env.npm_package_name || 'vue3-frame';
const projName = defaultName === 'vue3-frame' ? resolve(__dirname, './').split('\\').at(-1) ?? defaultName : defaultName;
// 排除打包的库，注意顺序，排在前的优先注入；键名是引入包的路径，键值是该库挂载到window的全局变量名称，区分大小写
const externalLibs: Record<string, string> = {
  'vue': 'Vue',
  'vue-router': 'VueRouter',
  'axios': 'axios',
  'element-plus': 'ElementPlus',
  'element-plus/dist/index.css': 'null',
  'element-plus/es/locale/lang/zh-cn': 'ElementPlusLocaleZhCn'
  // 'dayjs': 'dayjs',
  // 'echarts': 'echarts',
};
// 排除库配置
const externalConfig = new Map<string, ExternalConfig | string>([
  ['vue', {
    originalPath: 'vue/dist/vue.runtime.global.prod.js',
    originalPathDev: 'vue/dist/vue.runtime.global.js'
  }],
  ['vue-router', {
    originalPath: 'vue-router/dist/vue-router.global.prod.js',
    originalPathDev: 'vue-router/dist/vue-router.global.js'
  }],
  ['axios', 'axios/dist/axios.min.js'],
  ['element-plus', {
    originalPath: 'element-plus/dist/index.full.min.js',
    targetName: 'element.plus.js'
  }],
  ['element-plus/dist/index.css', {
    originalPath: 'element-plus/dist/index.css',
    targetName: 'element.plus.css'
  }],
  ['element-plus/es/locale/lang/zh-cn', {
    originalPath: 'element-plus/dist/locale/zh-cn.min.js',
    targetName: 'element.plus.zh-cn.js'
  }],
  ['dayjs', 'dayjs/dayjs.min.js'],
  ['echarts', 'echarts/dist/echarts.min.js']
]);

// 如果在注入阶段复制文件，那么优先级会低于CopyPlugin导致二次打包，需要提前到配置阶段（这里）执行
Object.keys(externalLibs).reverse().forEach(key => {
  if (externalConfig.has(key)) {
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const config = externalConfig.get(key)!;
    let path: string | undefined;
    let pathDev: string | undefined;
    let targetName: string | undefined;
    if (typeof config === 'string') {
      path = config;
    } else {
      path = config.originalPath;
      pathDev = config.originalPathDev;
      targetName = config.targetName;
    }
    copyToLib(path, targetName);
    pathDev && copyToLib(pathDev, targetName);
  } else {
    delete externalLibs[key];
  }
});

function getFileName(path: string): string {
  const paths = path.split('/');
  return paths.at(-1) ?? '';
}

function getFileExt(name: string): string | undefined {
  const index = name.lastIndexOf('.') + 1;
  return index && name.substring(index) || undefined;
}

function copyToLib(path: string, targetName?: string): void {
  const ext = getFileExt(path);
  if (ext && ['js', 'css'].includes(ext)) {
    const realTargetPath = resolve(__dirname, `./public/lib/${ext}/${targetName || getFileName(path)}`);
    const realOriginalPath = resolve(__dirname, './node_modules/' + path);
    // 如果文件不存在，那么复制文件到targetPath
    if (!existsSync(realTargetPath)) {
      const readStream = createReadStream(realOriginalPath);
      const writeStream = createWriteStream(realTargetPath);
      const rl = readline.createInterface({input: readStream});
      rl.on('line', line => {
        if (!line.startsWith('//# sourceMappingURL'))
          writeStream.write(line + '\n');
      });
      rl.on('close', () => {
        readStream.close();
        writeStream.close();
      });
    }
  }
}

function getInjectPath(isDev: boolean, config: ExternalConfig): ExternalRuntime | undefined {
  let path = config.originalPath;
  if (isDev && config.originalPathDev)
    path = config.originalPathDev;
  const ext = getFileExt(path);
  if (ext && ['js', 'css'].includes(ext)) {
    return {
      type: ext as any,
      targetPath: `./lib/${ext}/${config.targetName || getFileName(path)}`
    };
  }
}

class ExternalInjectPlugin {
  apply(compiler: Compiler): void {
    compiler.hooks.compilation.tap('ExternalInjectPlugin', compilation => {
      HtmlWebpackPlugin.getHooks(compilation).beforeAssetTagGeneration.tapAsync(
        'ExternalInjectPlugin',
        (data, cb) => {
          // Manipulate the content
          Object.keys(externalLibs).reverse().forEach(key => {
            if (externalConfig.has(key)) {
              // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
              const config = externalConfig.get(key)!;
              const injectPath = getInjectPath(process.env.NODE_ENV === 'development', typeof config === 'string' ? {originalPath: config} : config);
              injectPath && data.assets[injectPath.type].unshift(injectPath.targetPath);
            }
          });
          // Tell webpack to move on
          cb(null, data);
        }
      );
    });
  }
}

function getPlugins(isDev: boolean, isLib: boolean): Configuration['plugins'] {
  const plugins: Configuration['plugins'] = [
    new VueLoaderPlugin(),
    new DefinePlugin({
      'process.env.version': JSON.stringify(version),
      'process.env.projName': JSON.stringify(projName)
    })
  ];
  if (!isDev) {
    plugins.push(...[
      new MiniCssExtractPlugin({
        filename: isLib ? `${projName}.css` : 'bundle/[name].[contenthash].css',
        chunkFilename: 'chunks/[name].[contenthash].css'
      }),
      new CssMinimizerPlugin(),
      new BannerPlugin({
        banner: `@banner:${projName}_${version}`
      })
    ]);
  }
  if (!isLib) {
    plugins.push(...[
      new HtmlWebpackPlugin({
        template: resolve(__dirname, './public/index.html')
      }),
      new ExternalInjectPlugin()
    ]);
  }
  if (!isDev && !isLib) {
    plugins.push(new CopyPlugin({
      patterns: [
        {
          from: resolve(__dirname, './public'),
          to: resolve(__dirname, './dist'),
          globOptions: {
            ignore: ['**/public/pages/**', '**/public/index.html']
          },
          // Terser skip this file for minimization
          info: {minimized: true}
        }
      ]
    }));
  }
  return plugins;
}

function buildExternalItems(key: string, value: string): Record<string, string> {
  return {
    root: value,
    commonjs: key,
    commonjs2: key
  };
}

function buildExternals(): Record<string, object> {
  const externals: Record<string, object> = {};
  Object.entries(externalLibs).forEach(([key, value]) => {
    externals[key] = buildExternalItems(key, value);
  });
  return externals;
}

function handleRules(isOld: boolean, isDev: boolean, isLib: boolean): RuleSetRule[] {
  const rules: RuleSetRule[] = [{
    test: /\.tsx?$/i,
    use: [
      isOld ?
        {
          loader: 'babel-loader',
          options: {
            presets: [
              [
                // 不使用@babel/preset-typescript是因为有些语法仍然太高，例如类里面的方法不会转换为属性
                '@babel/preset-env',
                {
                  useBuiltIns: 'usage',
                  corejs: '3.35.1',
                  // 根据实际需求设定兼容性
                  targets: 'defaults'
                }
              ]
            ],
            // 插件来源于@babel/preset-typescript
            plugins: ['@babel/plugin-transform-typescript']
          }
        } :
        {
          loader: 'ts-loader',
          options: {
            appendTsSuffixTo: [/\.vue$/i],
            transpileOnly: isDev,
            compilerOptions: isDev ? {
              sourceMap: true
            } : {
              declaration: isLib
            }
          }
        }
    ]
  }];
  // 如果使用第三方umd包并且umd已经不需要兼容，js不需要转换
  if (isOld) {
    rules.push({
      test: /\.m?js$/i,
      exclude: [
        /core-js/,
        /webpack/
      ],
      use: [
        {
          loader: 'babel-loader',
          options: {
            presets: [
              [
                '@babel/preset-env',
                {
                  // 如果有第三方库无法按需补丁，那么设置为entry，并在入口添加 import 'core-js';
                  useBuiltIns: 'entry',
                  corejs: '3.35.1',
                  targets: 'default'
                }
              ]
            ]
          }
        }
      ]
    });
  }
  return rules;
}

export default function (env: EnvType): Configuration {
  // 是否为库模式（UMD）
  const isLib = env.lib === true;
  // 是否为开发模式
  const isDev = process.env.NODE_ENV === 'development';
  // 是否为库模式（ESM）
  const isEsm = isLib && env.esm === true;
  // 是否兼容
  const isOld = env.old === true;

  return {
    // 打包库时，更改入口为实际的组件或库入口，修改package.json中的name以定义类名，建议添加versionInfo
    experiments: isEsm ? {
      outputModule: true
    } : undefined,
    entry: resolve(__dirname, isLib ? './src/xxx.ts' : './src/index.ts'),
    output: isDev ? {pathinfo: false} : {
      library: isLib ? {
        name: isEsm ? undefined : projName.slice(0, 1).toUpperCase() + projName.slice(1),
        type: isEsm ? 'module' : 'umd',
        export: isEsm ? undefined : 'default'
      } : undefined,
      module: isEsm,
      path: resolve(__dirname, './dist'),
      filename: isLib ? `${projName}.js` : 'bundle/[name].[contenthash].js',
      chunkFilename: 'chunks/[name].[contenthash].js',
      assetModuleFilename: 'assets/[hash][ext]',
      clean: true,
      // 转换webpack带入的箭头函数
      environment: isOld ? {
        arrowFunction: false
      } : undefined
    },
    devtool: isDev ? 'source-map' : undefined,
    resolve: {
      // 在尝试查找文件时添加到请求中的扩展名
      extensions: ['.ts', '.js'],
      // 重定向模块请求
      alias: {
        '@': resolve(__dirname, './src')
      },
      symlinks: false
    },
    module: {
      rules: [
        {
          test: /\.vue$/i,
          use: 'vue-loader'
        },
        {
          test: /\.(png|svg|jpe?g|gif|bmp|webp|icon?|woff2?|eot|ttf|otf)$/i,
          type: 'asset/resource',
          parser: {
            // 4K以内资源内联到代码
            dataUrlCondition: {
              maxSize: 4 * 1024
            }
          }
        },
        ...handleRules(isOld, isDev, isLib),
        {
          test: /\.css$/i,
          use: [isDev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader']
        },
        {
          test: /\.less$/i,
          use: [isDev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
        }
      ]
    },
    plugins: getPlugins(isDev, isLib),
    devServer: isDev ? {
      // 自动打开浏览器
      open: true
    } : undefined,
    externals: [
      // 标准库项，下方可追加函数类非标准库
      isLib ? buildExternals() : externalLibs
    ],
    optimization: {
      minimize: !isDev,
      minimizer: isDev ? undefined : [
        new TerserPlugin({
          terserOptions: {
            format: {
              comments: /^! @banner:/i
            },
            compress: {
              drop_console: true,
              drop_debugger: true
            }
          },
          // 禁止产生license文件
          extractComments: false
        })
      ],
      runtimeChunk: isDev,
      removeAvailableModules: !isDev,
      removeEmptyChunks: !isDev,
      splitChunks: isDev ? false : undefined
    }
  };
}
